Skip to content

fix: CSV/JSON export via WebAppInterface + FileProvider (fixes #104)#186

Merged
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/csv-export-fileprovider
Jul 12, 2026
Merged

fix: CSV/JSON export via WebAppInterface + FileProvider (fixes #104)#186
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/csv-export-fileprovider

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Takes over from #109 (Erik asked @TimeToBuildBob to take over).

Problem

The original crash (#104) happens because the Android WebView can't trigger system downloads the way a desktop browser does. PR #109 addressed this with a JavascriptInterface bridge — the right approach — but used Uri.fromFile() which throws FileUriExposedException on Android 7.0+ (API 24+), causing a new crash.

Fix

  • WebUIFragment.kt: Adds WebAppInterface exposing downloadCSV() / downloadJSON() to window.Android, wired into the WebView via addJavascriptInterface. Uses FileProvider.getUriForFile() instead of Uri.fromFile() with FLAG_GRANT_READ_URI_PERMISSION so the viewer app can read the file.
  • AndroidManifest.xml: Declares the FileProvider with ${applicationId}.provider authority.
  • res/xml/file_paths.xml: Provider path config scoped to getExternalFilesDir(null).

Dependencies

Depends on ActivityWatch/aw-webui#532 (adds the window.Android.downloadCSV() call on the webui side).

Testing

Could not run the Android emulator in this environment. The pattern (FileProvider + @JavascriptInterface) is the standard Android approach for WebView→native file sharing since API 24.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds native Android handling for CSV and JSON exports from the WebView. The main changes are:

  • A window.Android bridge for CSV and JSON download requests.
  • Export files written under the app external files directory.
  • File sharing through an AndroidX FileProvider instead of file URIs.
  • Error handling for missing storage, write failures, and missing viewer apps.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt Adds the WebView export bridge and handles the main storage and viewer failure paths.
mobile/src/main/AndroidManifest.xml Registers the FileProvider used for sharing exported files.
mobile/src/main/res/xml/file_paths.xml Defines the FileProvider root for files written under external app storage.

Reviews (2): Last reviewed commit: "fix: harden downloadFile against path tr..." | Re-trigger Greptile

}

private fun downloadFile(content: String, filename: String, mimetype: String) {
val file = File(mContext.getExternalFilesDir(null), filename)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Filename Escapes Export Root

filename comes from JavaScript and is used as a path without validation. If the web UI passes a blank name or a name containing path separators such as ../, this can write outside the configured FileProvider root or target a directory, and the export then crashes in writeText or getUriForFile instead of opening the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47bea06: use File(filename).name to strip any path components from the JS-supplied name before creating the File. Empty names fall back to "export".

val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, mimetype)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NO_HISTORY)
mContext.startActivity(intent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing Viewer Crashes Export

startActivity can throw when no installed app handles text/csv or application/json. A valid CSV or JSON export on a device without a matching viewer can terminate the bridge call instead of showing a recoverable error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47bea06: wrapped startActivity in a try/catch (e: ActivityNotFoundException) so devices without a CSV/JSON viewer log the error and return cleanly instead of crashing the bridge.

}

private fun downloadFile(content: String, filename: String, mimetype: String) {
val file = File(mContext.getExternalFilesDir(null), filename)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unavailable External Files Directory

getExternalFilesDir(null) can return null, but this path passes it directly to File. On devices or profiles where external app storage is unavailable, starting an export throws before the CSV or JSON file is written.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47bea06: null-check getExternalFilesDir(null) with an early return and a Log.e when external storage is unavailable.


private fun downloadFile(content: String, filename: String, mimetype: String) {
val file = File(mContext.getExternalFilesDir(null), filename)
file.writeText(content)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Write Failure Terminates Export

writeText can throw for normal storage failures such as a full disk, denied write, invalid child path, or missing parent directory. Because the bridge does not catch that exception, a storage problem during export can crash the WebView bridge instead of reporting that the file could not be saved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47bea06: wrapped file.writeText(content) in a try/catch (e: Exception) that logs the error and returns early, so a full-disk or permission failure doesn't propagate up to the WebView bridge.

Adds a JavaScript bridge so the webui can trigger native file downloads
on Android. The original download-as-blob approach crashed because the
Android WebView cannot initiate system downloads the same way a browser does.

Changes:
- WebAppInterface: exposes downloadCSV() and downloadJSON() to window.Android
- Uses FileProvider (required API 24+) instead of Uri.fromFile(), which throws
  FileUriExposedException on Android 7.0 and later
- Adds res/xml/file_paths.xml for the provider's path config
- Declares the FileProvider in AndroidManifest.xml

Depends on ActivityWatch/aw-webui#532 (webui side of the bridge).

Fixes ActivityWatch#104
@TimeToBuildBob TimeToBuildBob force-pushed the fix/csv-export-fileprovider branch from d242bf9 to 47bea06 Compare July 12, 2026 17:46
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

@ErikBjare ErikBjare merged commit 49d9363 into ActivityWatch:master Jul 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants